/*
 * Copyright 2002-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.web.method.annotation;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.servlet.ServletException;

import org.springframework.beans.ConversionNotSupportedException;
import org.springframework.beans.TypeMismatchException;
import org.springframework.beans.factory.config.BeanExpressionContext;
import org.springframework.beans.factory.config.BeanExpressionResolver;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.core.MethodParameter;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ValueConstants;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.RequestScope;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

/**
 * Abstract base class for resolving method arguments from a named value.
 * Request parameters, request headers, and path variables are examples of named
 * values. Each may have a name, a required flag, and a default value.
 *
 * <p>Subclasses define how to do the following:
 * <ul>
 * <li>Obtain named value information for a method parameter
 * <li>Resolve names into argument values
 * <li>Handle missing argument values when argument values are required
 * <li>Optionally handle a resolved value
 * </ul>
 *
 * <p>A default value string can contain ${...} placeholders and Spring Expression
 * Language #{...} expressions. For this to work a
 * {@link ConfigurableBeanFactory} must be supplied to the class constructor.
 *
 * <p>A {@link WebDataBinder} is created to apply type conversion to the resolved
 * argument value if it doesn't match the method parameter type.
 *
 * @author Arjen Poutsma
 * @author Rossen Stoyanchev
 * @author Juergen Hoeller
 * @since 3.1
 */
public abstract class AbstractNamedValueMethodArgumentResolver implements HandlerMethodArgumentResolver {
	// 记录当前 Spring IoC 容器
	@Nullable
	private final ConfigurableBeanFactory configurableBeanFactory;
	// 使用bean定义进行表达式求值的上下文对象
	@Nullable
	private final BeanExpressionContext expressionContext;
	// 用于缓存找到的命名值信息
	private final Map<MethodParameter, NamedValueInfo> namedValueInfoCache = new ConcurrentHashMap<>(256);


	public AbstractNamedValueMethodArgumentResolver() {
		this.configurableBeanFactory = null;
		this.expressionContext = null;
	}

	/**
	 * Create a new {@link AbstractNamedValueMethodArgumentResolver} instance.
	 *
	 * @param beanFactory a bean factory to use for resolving ${...} placeholder
	 *                    and #{...} SpEL expressions in default values, or {@code null} if default
	 *                    values are not expected to contain expressions
	 */
	public AbstractNamedValueMethodArgumentResolver(@Nullable ConfigurableBeanFactory beanFactory) {
		this.configurableBeanFactory = beanFactory;
		// 初始化使用bean定义进行表达式求值的上下文对象
		// 仅在  this.configurableBeanFactory 被设置为非 null 时真正做初始化
		this.expressionContext =
				(beanFactory != null ? new BeanExpressionContext(beanFactory, new RequestScope()) : null);
	}

	// 接口 HandlerMethodArgumentResolver 所定义的方法，解析参数
	// 1. parameter 是要解析参数值的控制器方法参数
	// 2. mavContainer 是一个容器对象，用来保存控制器方法执行过程中各种决定信息，本方法并未使用它
	// 3. webRequest 包装了当前请求对象和响应对象，并且能返回 Servlet 规范定义的请求和响应对象
	// 4. binderFactory 是 数据绑定器工厂，用于生成数据绑定器，
	@Override
	@Nullable
	public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
										NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
		// 获取指定控制器方法参数  parameter 命名值描述信息 namedValueInfo
		NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
		MethodParameter nestedParameter = parameter.nestedIfOptional();
		//  namedValueInfo.name 是参数的名称字符串，不过该字符串可能是个表达式，需要进一步解析为
		// 最终的参数名称，下面的 resolveStringValue 语句就是对该名字进行表达式求值，从而得到解析后的
		// 控制器方法参数名称，此名称是从请求上下文中获取相应参数值的关键信息
		Object resolvedName = resolveStringValue(namedValueInfo.name);
		if (resolvedName == null) {
			throw new IllegalArgumentException(
					"Specified name must not resolve to null: [" + namedValueInfo.name + "]");
		}
		// 根据控制器方法参数名称从请求上下文中尝试分析得到该参数的参数值
		Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);
		if (arg == null) {
			// 没能从请求上下文中分析得到参数值的分支 ，也就是 arg 为 null 的情况
			if (namedValueInfo.defaultValue != null) {
				// 对该参数指定了缺省值的情况
				// 尝试应用缺省值,缺省值通过对 namedValueInfo.defaultValue 进行表达式求值得到
				arg = resolveStringValue(namedValueInfo.defaultValue);
				// 没能从请求上下文中分析得到参数值，并且没有指定缺省值， 并且被指定为必要参数的情况的处理
				// 也可以概括为必要参数的值缺失情况的处理
				// 参考注解 @RequestParam 的属性 required
				// AbstractNamedValueMethodArgumentResolver  这里的缺省实现是抛出异常 :
				// ServletRequestBindingException
			} else if (namedValueInfo.required && !nestedParameter.isOptional()) {
				handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
			}
			// 对 null 参数值的处理 :
			// 1. 如果参数类型为 Boolean 或者 boolean, 则 null 被理解成 Boolean.FALSE ;
			// 2. 如果参数类型为其他 Java 基本数据类型，则抛出异常 IllegalStateException,
			//  因为一个除了 boolean 之外的其他基本数据类型，是无法接受一个 null 值的 ;
			arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
		} else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
			// arg 为0长度字符串并且有缺省值可以应用的情况的处理
			arg = resolveStringValue(namedValueInfo.defaultValue);
		}
		// 现在， arg 变量值是经过各种解析将要应用到相应控制器方法参数的参数值,
		// 但是，从请求上下文中分析得到的 arg 通常是字符串类型，该类型跟目标控制器方法参数的类型
		// 通常都是不同的，所以现在需要尝试从值 arg 转换得到目标控制器方法参数类型的对象，
		// 例子 :
		// 1. arg 为字符串 "1" , 目标类型为 int, 则转换后的 arg 会是整数 1
		// 2. arg 为字符串 "test", 目标类型为 int, 则转换失败，抛出异常 TypeMismatchException
		if (binderFactory != null) {
			// 使用 binderFactory 数据绑定工厂创建数据绑定器，用于数据类型的转换
			WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
			// 尝试转换并捕捉相应的异常
			try {
				arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
				// 源类型 和 目标类型之间没有合适的转换器
			} catch (ConversionNotSupportedException ex) {
				throw new MethodArgumentConversionNotSupportedException(arg, ex.getRequiredType(),
						namedValueInfo.name, parameter, ex.getCause());
			} catch (TypeMismatchException ex) {
				throw new MethodArgumentTypeMismatchException(arg, ex.getRequiredType(),
						namedValueInfo.name, parameter, ex.getCause());
			}
		}
		// 上面是从请求中分析得到的参数值向目标控制器方法参数进行类型转换的逻辑，如果木有出现异常，
		// 程序执行会走到这里，此时 arg 对象是类型为目标控制器方法参数类型的对象，该值可以应用到
		// 目标方法参数了。
		// 在将 arg 返回给调用者真正应用到目标方法参数之前，当前对象还可以通过 handleResolvedValue
		// 方法对 arg 做一番调整。不过在抽象类 AbstractNamedValueMethodArgumentResolver 中，
		// 此方法是一个空方法，也就是不做任何调整，但是具体实现子类可以覆盖实现该方法实现自己的逻辑。
		handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);
		// 从请求上下文中分析参数值，应用缺省值，处理 null 值，源类型到目标类型的类型转换都已经做完了,
		// 大功告成，将此分析得到的参数值 arg 返回给调用者让其继续接下来的后续逻辑
		return arg;
	}

	/**
	 * Obtain the named value for the given method parameter.
	 */
	private NamedValueInfo getNamedValueInfo(MethodParameter parameter) {
		NamedValueInfo namedValueInfo = this.namedValueInfoCache.get(parameter);
		if (namedValueInfo == null) {
			namedValueInfo = createNamedValueInfo(parameter);
			namedValueInfo = updateNamedValueInfo(parameter, namedValueInfo);
			this.namedValueInfoCache.put(parameter, namedValueInfo);
		}
		return namedValueInfo;
	}

	/**
	 * Create the {@link NamedValueInfo} object for the given method parameter. Implementations typically
	 * retrieve the method annotation by means of {@link MethodParameter#getParameterAnnotation(Class)}.
	 *
	 * @param parameter the method parameter
	 * @return the named value information
	 */
	protected abstract NamedValueInfo createNamedValueInfo(MethodParameter parameter);

	/**
	 * Create a new NamedValueInfo based on the given NamedValueInfo with sanitized values.
	 */
	private NamedValueInfo updateNamedValueInfo(MethodParameter parameter, NamedValueInfo info) {
		String name = info.name;
		if (info.name.isEmpty()) {
			name = parameter.getParameterName();
			if (name == null) {
				throw new IllegalArgumentException(
						"Name for argument type [" + parameter.getNestedParameterType().getName() +
								"] not available, and parameter name information not found in class file either.");
			}
		}
		String defaultValue = (ValueConstants.DEFAULT_NONE.equals(info.defaultValue) ? null : info.defaultValue);
		return new NamedValueInfo(name, info.required, defaultValue);
	}

	/**
	 * Resolve the given annotation-specified value,
	 * potentially containing placeholders and expressions.
	 */
	@Nullable
	private Object resolveStringValue(String value) {
		if (this.configurableBeanFactory == null) {
			return value;
		}
		String placeholdersResolved = this.configurableBeanFactory.resolveEmbeddedValue(value);
		BeanExpressionResolver exprResolver = this.configurableBeanFactory.getBeanExpressionResolver();
		if (exprResolver == null || this.expressionContext == null) {
			return value;
		}
		return exprResolver.evaluate(placeholdersResolved, this.expressionContext);
	}

	/**
	 * Resolve the given parameter type and value name into an argument value.
	 *
	 * @param name      the name of the value being resolved
	 * @param parameter the method parameter to resolve to an argument value
	 *                  (pre-nested in case of a {@link java.util.Optional} declaration)
	 * @param request   the current request
	 * @return the resolved argument (may be {@code null})
	 * @throws Exception in case of errors
	 */
	@Nullable
	protected abstract Object resolveName(String name, MethodParameter parameter, NativeWebRequest request)
			throws Exception;

	/**
	 * Invoked when a named value is required, but {@link #resolveName(String, MethodParameter, NativeWebRequest)}
	 * returned {@code null} and there is no default value. Subclasses typically throw an exception in this case.
	 *
	 * @param name      the name for the value
	 * @param parameter the method parameter
	 * @param request   the current request
	 * @since 4.3
	 */
	protected void handleMissingValue(String name, MethodParameter parameter, NativeWebRequest request)
			throws Exception {

		handleMissingValue(name, parameter);
	}

	/**
	 * Invoked when a named value is required, but {@link #resolveName(String, MethodParameter, NativeWebRequest)}
	 * returned {@code null} and there is no default value. Subclasses typically throw an exception in this case.
	 *
	 * @param name      the name for the value
	 * @param parameter the method parameter
	 */
	protected void handleMissingValue(String name, MethodParameter parameter) throws ServletException {
		throw new ServletRequestBindingException("Missing argument '" + name +
				"' for method parameter of type " + parameter.getNestedParameterType().getSimpleName());
	}

	/**
	 * A {@code null} results in a {@code false} value for {@code boolean}s or an exception for other primitives.
	 */
	@Nullable
	private Object handleNullValue(String name, @Nullable Object value, Class<?> paramType) {
		if (value == null) {
			if (Boolean.TYPE.equals(paramType)) {
				return Boolean.FALSE;
			} else if (paramType.isPrimitive()) {
				throw new IllegalStateException("Optional " + paramType.getSimpleName() + " parameter '" + name +
						"' is present but cannot be translated into a null value due to being declared as a " +
						"primitive type. Consider declaring it as object wrapper for the corresponding primitive type.");
			}
		}
		return value;
	}

	/**
	 * Invoked after a value is resolved.
	 *
	 * @param arg          the resolved argument value
	 * @param name         the argument name
	 * @param parameter    the argument parameter type
	 * @param mavContainer the {@link ModelAndViewContainer} (may be {@code null})
	 * @param webRequest   the current request
	 */
	protected void handleResolvedValue(@Nullable Object arg, String name, MethodParameter parameter,
									   @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest) {
	}


	/**
	 * Represents the information about a named value, including name, whether it's required and a default value.
	 */
	protected static class NamedValueInfo {

		private final String name;

		private final boolean required;

		@Nullable
		private final String defaultValue;

		public NamedValueInfo(String name, boolean required, @Nullable String defaultValue) {
			this.name = name;
			this.required = required;
			this.defaultValue = defaultValue;
		}
	}

}
